feat(django): Support span streaming#6248
Conversation
Codecov Results 📊✅ 282 passed | Total: 282 | Pass Rate: 100% | Execution Time: 43.82s All tests are passing successfully. ❌ Patch coverage is 0.95%. Project has 14983 uncovered lines. Files with missing lines (11)
Generated by Codecov Action |
There was a problem hiding this comment.
Span thread.id assertion will crash: UnwrappedItem isn't subscriptable and span payload has no contexts key (tests/integrations/django/asgi/test_asgi.py:231)
In tests/integrations/django/asgi/test_asgi.py (around L231–L235) the span_streaming branch does:
spans = [item for item in items if item.type == "span"]
for span in spans:
trace_context = span["contexts"]["trace"]
assert str(data["active"]) == trace_context["attributes"]["thread.id"]Two problems:
capture_items(tests/conftest.py ~L340) returnsUnwrappedItemdataclass instances (fieldstype,payload) with no__getitem__, sospan["contexts"]raisesTypeError: 'UnwrappedItem' object is not subscriptable. Every other call site in the repo correctly usesitem.payload for item in items if item.type == "span".- Even after switching to
span.payload, the unwrapped span payload follows the OTel v2 span schema (trace_id,span_id,name,attributes, ... — seetests/tracing/test_span_batcher.py~L407–420). There is nocontexts.traceenvelope around it, sospan.payload["contexts"]would raiseKeyError.
As long as at least one span is emitted for /sync/thread_ids / /async/thread_ids (which is the point of the test), the assertion crashes before it can validate the streaming thread-id behavior, defeating the purpose of the new test parametrization.
Verification
Inspected tests/conftest.py ~L340 (UnwrappedItem is a plain @dataclass with type/payload, no __getitem__) and tests/conftest.py ~L367–L371 (for span items it stores the per-item dict from item.payload.json["items"] directly into payload, not under a contexts key). Confirmed via tests/tracing/test_span_batcher.py ~L407 that streaming span payloads are flat OTel-style (trace_id, span_id, attributes, ...). All other span-iterating tests in the repo (anthropic, sqlalchemy, fastmcp, openai, tracing) use item.payload for item in items if item.type == "span" and access attributes directly.
Test loop may pass vacuously when no spans are captured (tests/integrations/django/asgi/test_asgi.py:233)
There is no assertion that spans is non-empty before the for span in spans loop, so if the streaming path emits no spans the assertions inside the loop are never executed and the test passes silently — unlike the else branch which has assert len(transactions) == 1.
Verification
In the else branch (lines 248, 263) assert len(profiles) == 1 and assert len(transactions) == 1 guard both loops. The span_streaming=True branch has no equivalent guard, so an empty spans list produces a vacuously-passing test.
test_sql_queries and test_sql_dict_query_params never configure SDK for streaming mode (tests/integrations/django/test_basic.py:700)
Both tests parametrize span_streaming but their sentry_init call only sets _experiments={"record_sql_params": True} — the "trace_lifecycle": "stream" key is never set, so the span_streaming=True variant runs with the default (static) lifecycle and isn't testing streaming at all.
Verification
In test_queryset_repr (line ~553) the sentry_init correctly uses _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}. In test_sql_queries (line ~697) the sentry_init uses only _experiments={"record_sql_params": True} with no trace_lifecycle key regardless of the span_streaming value. Same pattern in test_sql_dict_query_params (~line 750). The span_streaming=True branch uses capture_items("event") instead of capture_events(), but because the SDK is running in static mode, behavior is identical to the False branch and actual streaming code paths are never exercised.
Identified by Warden code-review
There was a problem hiding this comment.
asyncpg streaming path sets deprecated db.system instead of db.system.name
In sentry_sdk/integrations/asyncpg.py, _set_db_data and _wrap_connect_addr both use SPANDATA.DB_SYSTEM ("db.system") in the streaming path; they should use SPANDATA.DB_SYSTEM_NAME ("db.system.name") like the sqlalchemy integration does.
Verification
asyncpg.pyline 266:set_value(SPANDATA.DB_SYSTEM, "postgresql")uses the same deprecated key for both streaming and non-streaming paths.asyncpg.pyline 213:SPANDATA.DB_SYSTEM: "postgresql"is placed directly in the streamingspan_attributesdict.sqlalchemy.py_set_db_datacorrectly branches:span.set_attribute(SPANDATA.DB_SYSTEM_NAME, ...)for StreamedSpan vsspan.set_data(SPANDATA.DB_SYSTEM, ...)for Span.- The PR description explicitly states to use
db.system.nameinstead ofdb.systemin the streaming path. - Test
test_asyncpg.pyline 1401 confirms the wrong behaviour by assertingbind_exec_span["attributes"]["db.system"] == "postgresql"in the streaming case — the tests are aligned to the bug, not the spec.
asyncpg streaming path sets deprecated db.name instead of db.namespace
In sentry_sdk/integrations/asyncpg.py, _set_db_data (line 279) and _wrap_connect_addr (line 216) both use SPANDATA.DB_NAME ("db.name") in the streaming path; they should use SPANDATA.DB_NAMESPACE ("db.namespace") like the sqlalchemy integration does.
Verification
asyncpg.pyline 279:set_value(SPANDATA.DB_NAME, database)is called with the same deprecated key for both streaming and non-streaming paths via the unifiedset_valuealias.asyncpg.pyline 216:SPANDATA.DB_NAME: databaseis placed directly in the streamingspan_attributesdict inside_wrap_connect_addr.sqlalchemy.py_set_db_datacorrectly usesspan.set_attribute(SPANDATA.DB_NAMESPACE, db_name)for StreamedSpan andspan.set_data(SPANDATA.DB_NAME, ...)for Span.- The PR description explicitly states to use
db.namespaceinstead ofdb.namein the streaming path. - Test
test_asyncpg.pyline 1406 assertsbind_exec_span["attributes"]["db.name"] == PG_NAMEin the streaming path, confirming the tests encode the wrong attribute name. consts.pymarksDB_NAME = "db.name"as deprecated with a note to useDB_NAMESPACEinstead.
Identified by Warden find-bugs
There was a problem hiding this comment.
asyncpg streaming connect span sets deprecated db.system/db.name instead of db.system.name/db.namespace
The asyncpg streaming connect span (and _set_db_data) uses SPANDATA.DB_SYSTEM (db.system) and SPANDATA.DB_NAME (db.name) — both marked deprecated in favor of DB_SYSTEM_NAME (db.system.name) and DB_NAMESPACE (db.namespace) — while the Django integration correctly uses the new names in the same PR.
Verification
In sentry_sdk/integrations/asyncpg.py the streaming connect path (lines ~213–216) builds span_attributes with keys SPANDATA.DB_SYSTEM and SPANDATA.DB_NAME. The _set_db_data helper (lines ~266, ~279) also uses the same deprecated constants when invoked with a StreamedSpan. By contrast, sentry_sdk/integrations/django/__init__.py's _set_db_data correctly branches on isinstance(span, StreamedSpan) and uses SPANDATA.DB_SYSTEM_NAME / SPANDATA.DB_NAMESPACE for streaming spans. The test_asyncpg.py streaming tests only assert on breadcrumbs (CRUMBS_CONNECT contains db.system and db.name) and never check the span attributes, so this inconsistency is not caught by the test suite.
Identified by Warden find-bugs
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 285de05. Configure here.
There was a problem hiding this comment.
asyncpg streaming path uses deprecated db.system and db.name attributes instead of db.system.name and db.namespace
In asyncpg.py _set_db_data, set_value is bound to span.set_attribute for StreamedSpan but still uses SPANDATA.DB_SYSTEM (db.system) and SPANDATA.DB_NAME (db.name) — the deprecated keys — instead of SPANDATA.DB_SYSTEM_NAME and SPANDATA.DB_NAMESPACE required by the streaming path. The django and sqlalchemy integrations in this same PR correctly use the new keys.
Verification
Checked sentry_sdk/integrations/asyncpg.py lines 264-279 (_set_db_data): set_value = span.set_attribute if isinstance(span, StreamedSpan) else span.set_data, then set_value(SPANDATA.DB_SYSTEM, ...) (line 266) and set_value(SPANDATA.DB_NAME, ...) (line 279) — both using the deprecated key names for both streaming and non-streaming paths. SPANDATA.DB_SYSTEM = 'db.system' (deprecated, per consts.py:511-514) and SPANDATA.DB_SYSTEM_NAME = 'db.system.name' (new, per consts.py:521). Same for DB_NAME vs DB_NAMESPACE. By contrast, sqlalchemy.py _set_db_data (lines ~144-171) and django/__init__.py _set_db_data (lines 776-820) both branch on isinstance(span, StreamedSpan) and use DB_SYSTEM_NAME/DB_NAMESPACE for the streaming path. No grep match for DB_SYSTEM_NAME or DB_NAMESPACE in asyncpg.py confirms neither is used.
Identified by Warden find-bugs
There was a problem hiding this comment.
record_sql_queries silently drops SQL debug data (db.params, db.executemany) for streaming spans
In tracing_utils.py, the data dict (containing db.params, db.paramstyle, db.executemany, db.cursor) is built then applied to the span via span.set_data() in non-streaming mode, but the streaming path yields the span without setting any of these attributes, silently losing the data even when record_sql_params is enabled.
Verification
Checked sentry_sdk/tracing_utils.py lines 141-171. The data dict is unconditionally populated (conditioned on params_list, paramstyle, executemany, record_cursor_repr). In the else branch (non-streaming), for k, v in data.items(): span.set_data(k, v) correctly applies them. In the if has_span_streaming_enabled(...) branch (streaming), the with sentry_sdk.traces.start_span(...) block immediately yields the span with no equivalent attribute-setting loop. Callers like Django's execute() and asyncpg's _record() pass params_list and executemany=True/record_cursor_repr=True expressly to have them recorded, but in streaming mode none of this data reaches the span.
Identified by Warden find-bugs
sentrivana
left a comment
There was a problem hiding this comment.
Looks good to me, including the data that we're dropping in span first (like template context) -- we can re-add if there's demand for it.

Description
There are 3 event processors in the integration. None of these need to be ported:
process_django_templates()exits early if the event is not an exceptionasgi_request_event_processor()adds request info (out of scope for span first).wsgi_request_event_processor()adds request info (out of scope for span first).In the streaming path, use
db.namespaceinstead ofdb.namedb.system.nameinstead ofdb.systemdb.operation.nameinstead ofdb.operationmiddleware.nameinstead ofdjango.middleware_namecode.function.nameinstead ofsignalcode.filepathinstead ofcode.file.pathcode.line.numberinstead ofcode.linenoDropped attributes:
django.function_namecontextAdapting Tests
sedcommands used for converting transaction context managers:sed commands used for converting specific attributes:
sedcommands used for converting event capture:sedcommands used for convertingop:sedcommands used for converting origin:sedcommands used for convertingdescription:sedcommands used for convertingdatatoattributes:sedcommands for converting trace id:sedcommands used for converting timestamps:other test changes:
Issues
Closes #6015
Reminders
tox -e linters.feat:,fix:,ref:,meta:)